package com.baymax.test.scene.internal

import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.io.FileSystem
import okio.Buffer
import okio.ForwardingSource
import okio.HashingSink
import okio.Sink
import okio.Source
import okio.blackholeSink
import okio.buffer
import okio.sink
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resumeWithException

class Downloader(
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
    val client: OkHttpClient = OkHttpClient()
) {
    private val TAG = "Downloader"

    internal suspend fun download(
        context: Context,
        fileName: String,
        url: String,
        md5: String,
        progressChange: (Int, Boolean) -> Unit = { p, d ->
            Log.d(
                TAG,
                "progressChange $p ,success $d"
            )
        }
    ): Result<File> {
        val dir = File(context.externalCacheDir, "apk")
        yield()
        gcDir(dir)
        yield()
        Log.d(TAG, "download: $fileName ,$url ,$md5")
        val copyTo = File(dir, "${fileName}.apk")
        copyTo.parentFile?.takeIf { !it.exists() }?.mkdirs()
        yield()
        if (copyTo.exists()) {
            val equal = withContext(Dispatchers.IO) {
                //FileSystem.SYSTEM.source(copyTo)
                //.use { it.buffer().md5().hex().lowercase()}
                val localFileMd5 = FileSystem.SYSTEM.source(copyTo).buffer().use { source ->
                    HashingSink.md5(blackholeSink()).use { hs ->
                        source.readAll(hs)
                        hs.hash.hex()
                    }
                }
                val localEqual = md5.trim().equals(localFileMd5, true)
                Log.d(TAG, "checkLocal: $localFileMd5 == $md5 = $localEqual")
                localEqual
            }
            if (equal) {
                Log.d(TAG, "checkLocal: return cache file")
                return Result.success(copyTo)
            }
        }
        Log.d(TAG, "download:start $fileName ,$url ,$md5")
        yield()
        progressChange(0, false)
        val realSink = HashingSink.md5(FileSystem.SYSTEM.sink(copyTo))
        downloadWTF(
            requireNotNull(url.toHttpUrlOrNull()),
            { realSink },
            progressChange
        ).await()
        Log.i(TAG, "download: download ok ${copyTo.length()}")
        yield()
        if (md5.isNotBlank()) {
            val fileMd5 = realSink.hash.hex()
            val equal = md5.trim().equals(fileMd5, true)
            Log.i(TAG, "download: checkMd5  $md5 == $fileMd5  = $equal")
            if (!equal) {
                copyTo.delete()
                return Result.failure(Throwable("md5 check error"))
            }
        }
        return Result.success(copyTo)
    }

    private fun gcDir(dir: File) {
        val current = System.currentTimeMillis()
        val duration = TimeUnit.DAYS.toMillis(3)
        kotlin.runCatching {
            dir.walk().filter {
                val p = (current - it.lastModified())
                return@filter p >= duration
            }.forEach {
                it.deleteRecursively()
            }
            Log.w(TAG, "gcDir success")
        }.onFailure {
        }
    }


    internal suspend fun download2(
        url: String,
        saver: (Response) -> Sink?,
        progressChange: (Int, Boolean) -> Unit = { p, d ->
            Log.d(
                TAG,
                "progressChange $p ,success $d"
            )
        }
    ): Result<Unit> {
        var f :File? = null
        Log.d(TAG, "download:start $url")
        yield()
        progressChange(0, false)
        downloadWTF(
            requireNotNull(url.toHttpUrlOrNull()),
            saver = saver,
            progressChange
        ).await()
        return Result.success(Unit)
    }


    private suspend fun downloadWTF(
        httpUrl: HttpUrl,
        saver: (Response) -> Sink?,
        progressChange: (Int, Boolean) -> Unit = { p, d ->
            Log.d(
                TAG,
                "progressChange $p ,success $d"
            )
        }
    ): Deferred<Unit> {
        return coroutineScope {
            async(dispatcher) {
                suspendCancellableCoroutine { con ->
                    val request =
                        Request.Builder().url(httpUrl).addHeader("Accept-Encoding", "identity")
                            .build()
                    val download = client.newCall(request)
                    download.enqueue(object : Callback {
                        override fun onFailure(call: Call, e: IOException) {
                            Log.w(TAG, "download: onFailure", e)
                            con.resumeWithException(e)
                        }

                        override fun onResponse(call: Call, response: Response) {
                            Log.i(TAG, "download: onResponse $response")
                            if (response.code != 200) {
                                con.resumeWithException(Throwable(response.toString()))
                                return
                            }
                            val headers = response.headers
                            Log.d(TAG, "resp headers $headers")
                            val body = response.body
                            if (body != null) {
                                val progressSource =
                                    ProgressSource(
                                        body.contentLength(),
                                        body.source(),
                                        progressChange
                                    )
                                progressSource.use {
                                    saver(response)?.buffer()?.use { s -> s.writeAll(progressSource) }
                                }
                                con.resumeWith(Result.success(Unit))
                            } else {
                                Log.w(TAG, "download: onResponse no body source")
                                con.resumeWithException(Throwable("no body source"))
                            }
                        }
                    })
                    con.invokeOnCancellation { download.cancel() }
                }
            }
        }
    }


    class ProgressSource(
        private val total: Long,
        source: Source,
        private val progressChange: (Int, Boolean) -> Unit,
    ) : ForwardingSource(source) {
        private var totalBytesRead = 0L
        private var progress = 0 //0..100

        init {
            if (total <= 0) {
                progressChange(100, true)
            }
        }

        override fun read(sink: Buffer, byteCount: Long): Long {
            val bytesRead = super.read(sink, byteCount)
            val isDone = bytesRead == -1L
            if (isDone) {
                progressChange(100, true)
            } else {
                totalBytesRead += bytesRead
                val per = (totalBytesRead * 100f / total).toInt()
                if (per > progress) {
                    progressChange(per, false)
                    progress = per
                }
            }
            return bytesRead
        }
    }

}
